require('dotenv').config();
const Mixer = require('audio-mixer').Mixer;
const Readable = require('stream').Readable;
const Speaker = require('speaker');
const tone = require('tonegenerator');
const BFX = require('bitfinex-api-node');

const fs = require('fs');
const path = require('path');
const lame = require('lame');

const Discord = require('discord.js');
const voiceclient = new Discord.Client();

const config = {
  centsPerBtc: 25,
  initialInputs: 10,
  logCount: 10,
  logPeriod: 1 * 60 * 1000,
  maxListeners: 30,
  minTradeVolume: 0.01,
  octaveRange: 2,
  rootFreq: 220,
  output: {
    channels: 1,
    bitDepth: 16,
    sampleRate: 44100
  }
};

const sources = [
  {
    pair: 'BTCUSD',
    shape: 'square'
  },
  {
    pair: 'ETHBTC',
    shape: 'triangle'
  }
];

const mixer = new Mixer({
  channels: config.output.channels
});

// File output
// FIXME: Including zero data? Each MP3 is only as long as it's content was active?
const encoder = new lame.Encoder({
  // Input
  channels: config.output.channels,
  bitDepth: config.output.bitDepth,
  sampleRate: config.output.sampleRate,
  // Output
  bitRate: 128,
  outSampleRate: 22050,
  mode: lame.MONO // STEREO (default), JOINTSTEREO, DUALCHANNEL or MONO
});
mixer.pipe(encoder);

// Periodically switch log files.
const saveAudio = () => {
  // Rollover older logs.
  const files = fs.readdirSync(__dirname + '/logs');
  files.forEach(file => {
    const timestamp = parseInt(path.basename(file, '.mp3'), 10);
    if (
      timestamp > 0 &&
      Date.now() - timestamp > config.logPeriod * config.logCount
    )
      fs.unlinkSync(__dirname + '/logs/' + file);
  });

  const file = fs.createWriteStream(__dirname + '/logs/' + Date.now() + '.mp3');
  encoder.pipe(file);
  setTimeout(() => {
    encoder.unpipe(file);
    file.end();
    saveAudio(); // Pipe next log file.
  }, config.logPeriod);
};
saveAudio();

// Output audio
// FIXME: Discord doesn't receive any audio until terminated?
if (process.env.DISCORD_TOKEN && process.env.DISCORD_CLIENTID) {
  console.log('Output: Discord');

  voiceclient.on('ready', () => {
    console.log(`Logged in as ${voiceclient.user.tag}!`);

    console.log(
      'Invite: ',
      'https://discordapp.com/oauth2/authorize?client_id=' +
        process.env.DISCORD_CLIENTID +
        '&scope=bot'
    );
  });

  voiceclient.on('message', msg => {
    if (msg.content === '/join') {
      if (msg.member.voiceChannel) {
        msg.member.voiceChannel
          .join()
          .then(connection => {
            console.log('Connected to "' + msg.member.voiceChannel.name + '"');
            msg.reply('Connected to "' + msg.member.voiceChannel.name + '"');
            //const files = fs.readdirSync(__dirname + '/logs');
            //.playFile(__dirname + '/logs/' + files[1]);

            // FIXME: Not playing until `end`?
            const broadcast = voiceclient
              .createVoiceBroadcast()
              .playStream(encoder);
            const dispatcher = connection.playBroadcast(broadcast);

            dispatcher.player.on('warn', console.warn);
            dispatcher.on('warn', console.warn);
            dispatcher.on('error', console.error);
            dispatcher.on('close', console.log);
            dispatcher.on('finish', console.log);
            dispatcher.on('end', console.log);
            connection.setSpeaking(true);
          })
          .catch(console.log);
      } else {
        msg.reply('You must join a voice channel first!');
      }
    }
  });

  voiceclient.login(process.env.DISCORD_TOKEN);
} else {
  console.log('Output: Speaker');

  const speaker = new Speaker(config.output);
  mixer.pipe(speaker);
}

let inputs = [];
for (let i = 0; i < config.initialInputs; i++) {
  let input = mixer.input(config.output);
  input.setMaxListeners(config.maxListeners);
  inputs.push(input);
}

const playTone = (freq = 440, dur = 1.0, vol = 0, type = 'sine') => {
  // FIXME: Unlike 8-bit data, the volumes does not start at 0, but at -32768.
  vol = Math.min(tone.MAX_16, parseInt(vol, 10));
  //console.log('playTone', type, freq, vol);

  const tonedata = tone({
    freq: freq,
    lengthInSecs: dur,
    volume: vol,
    rate: config.output.sampleRate,
    shape: type
  });

  let input;
  // Check for available existing input.
  for (let i = 0; i < inputs.length; i++) {
    if (inputs[i].buffer.length === 0) {
      input = inputs[i];
      break;
    }
  }

  // Create new input as required.
  if (!input) {
    input = mixer.input(config.output);
    input.setMaxListeners(config.maxListeners);
    inputs.push(input);
    console.log('Create Channel: ', inputs.length);
  }

  const readable = new Readable();
  readable._read = () => {};
  readable.push(new Buffer(tonedata));
  readable.push(null); // Null terminator.
  readable.pipe(
    input,
    { end: false } // Leave input live
  );
};

const bfx = new BFX({
  //apiKey: "...",
  //apiSecret: "...",

  ws: {
    autoReconnect: true,
    seqAudit: true,
    packetWDDelay: 10 * 1000
  }
});

const ws = bfx.ws();

ws.on('error', err => console.log(err));
ws.on('open', () => {
  for (let i = 0; i < sources.length; i++) {
    if (!sources[i].pair) return;
    ws.subscribeTrades(sources[i].pair);
  }
});

for (let i = 0; i < sources.length; i++) {
  if (!sources[i].pair) return;
  ws.onTradeEntry({ pair: sources[i].pair }, trades => {
    //console.log(`te: ${JSON.stringify(trades)}`);
    //te: [[300857924,1538431082884,-0.003,6596.127804]]
    let volume = 0;
    let price = 0;
    trades.forEach(trade => {
      volume += trade[2];
      price += trade[3];
    });
    price /= trades.length;

    if (Math.abs(volume) > config.minTradeVolume) {
      // Convert volume to BTC if required.
      let volBtc = volume;
      const volPair = sources[i].pair.substr(sources[i].pair.length - 3);
      if (volPair === 'BTC') {
        volBtc *= price;
      }

      // Create pitch and duration from BTC volume.
      let pitch;
      let base = volBtc < 0 ? config.rootFreq - 50 : config.rootFreq + 50;
      console.log(
        new Date().toISOString(),
        sources[i].pair,
        price.toFixed(5),
        volBtc
      );
      /* TODO: What about using stddev from average? BB style.
          On volume, not price.
       */
      pitch = Math.floor(base + config.centsPerBtc * volBtc);
      pitch = Math.min(base * config.octaveRange, pitch);
      pitch = Math.max(base / config.octaveRange, pitch);
      let duration = 25 / pitch; // An even X cycles at pitch, no clip.

      volBtc = Math.abs(volBtc);
      playTone(pitch, duration, (volBtc / 300) * tone.MAX_16, sources[i].shape);
    }
  });
}

ws.open();
/*
// Test tone.
setInterval(() => {
  playTone(440, 0.25, 100);
}, 3000);
*/
